﻿using Microsoft.EntityFrameworkCore.Metadata;
using OA.Infrastructure.Enums;
using OA.Infrastructure.Logger.Abstractions;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace OA.Infrastructure.Logger;

public class TrackerEntry : ITrackerEntry
{
    public Dictionary<string, string> Tables { get; } = new Dictionary<string, string>();
    public OperationCategory OperationCategory { get; private set; }
    public Dictionary<string, Dictionary<string, object?>?> OriginalValues { get; private set; } = new Dictionary<string, Dictionary<string, object?>?>();
    public Dictionary<string, Dictionary<string, object?>?> CurrentValues { get; private set; } = new Dictionary<string, Dictionary<string, object?>?>();
    public Dictionary<string, Dictionary<string, object?>> KeyValues { get; private set; } = new Dictionary<string, Dictionary<string, object?>>();
    public Dictionary<string, Dictionary<string, object?>> Fields { get; private set; } = new Dictionary<string, Dictionary<string, object?>>();
    public Dictionary<string, List<PropertyEntry>?> TemporaryProperties { get; set; } = new Dictionary<string, List<PropertyEntry>?>();

    public TrackerEntry(EntityEntry entityEntry, ILoggerOptions loggerOptions)
    {
        if (entityEntry.Metadata.ClrType.Name.IndexOf("Dictionary") != -1)
        {
            AnalyzeAssociate(entityEntry, loggerOptions);
        }
        else
        {
            Tables.Add(entityEntry.Metadata.GetTableName()!, entityEntry.GetDisplayName());

            var currentTableName = Tables.First().Key;

            InitializeInternalEntry(entityEntry, currentTableName);

            Analyze(entityEntry, currentTableName, loggerOptions);
        }
    }

    private void InitializeInternalEntry(EntityEntry entityEntry, string tableName)
    {
        if (entityEntry.Properties.Any(x => x.IsTemporary))
        {
            TemporaryProperties.Add(tableName, null);
        }

        if (entityEntry.State == EntityState.Added)
        {
            OperationCategory = OperationCategory.Add;
            CurrentValues.Add(tableName, null);
        }
        else if (entityEntry.State == EntityState.Deleted)
        {
            OperationCategory = OperationCategory.Delete;
            OriginalValues.Add(tableName, null);
        }
        else if (entityEntry.State == EntityState.Modified)
        {
            OperationCategory = OperationCategory.Update;
            OriginalValues.Add(tableName, null);
            CurrentValues.Add(tableName, null);
        }

        Fields.TryAdd(tableName, new Dictionary<string, object?>());
        KeyValues.TryAdd(tableName, new Dictionary<string, object?>());
    }

    private void Analyze(EntityEntry entityEntry, string tableName, ILoggerOptions loggerOptions)
    {
        foreach (var propertyEntry in entityEntry.Properties)
        {
            if (loggerOptions.PropertyFilters.Any(f => !f.Invoke(entityEntry, propertyEntry)))
            {
                continue;
            }

            var columnName = propertyEntry.GetColumnName();

            if (propertyEntry.IsTemporary)
            {
                Fields[tableName].TryAdd(columnName, propertyEntry.GetDisplayName(columnName));

                if (TemporaryProperties![tableName] is null)
                {
                    TemporaryProperties![tableName] = new List<PropertyEntry>();
                }

                TemporaryProperties![tableName]!.Add(propertyEntry);
                continue;
            }

            if (propertyEntry.Metadata.IsPrimaryKey())
            {
                Fields[tableName].TryAdd(columnName, propertyEntry.GetDisplayName(columnName));
                KeyValues[tableName].Add(columnName, propertyEntry.CurrentValue);
            }

            switch (entityEntry.State)
            {
                case EntityState.Added:
                    Fields[tableName].TryAdd(columnName, propertyEntry.GetDisplayName(columnName));
                    if (CurrentValues![tableName] is null)
                    {
                        CurrentValues![tableName] = new Dictionary<string, object?>();
                    }

                    CurrentValues![tableName]!.Add(columnName, propertyEntry.CurrentValue);
                    break;

                case EntityState.Deleted:
                    Fields[tableName].TryAdd(columnName, propertyEntry.GetDisplayName(columnName));
                    if (OriginalValues![tableName] is null)
                    {
                        OriginalValues![tableName] = new Dictionary<string, object?>();
                    }

                    OriginalValues![tableName]!.Add(columnName, propertyEntry.OriginalValue);
                    break;

                case EntityState.Modified:
                    if (propertyEntry.IsModified || loggerOptions.SaveUnChangedProperties)
                    {
                        if (propertyEntry.OriginalValue is not null && !propertyEntry.OriginalValue.Equals(propertyEntry.CurrentValue) ||
                            propertyEntry.OriginalValue is null && propertyEntry.CurrentValue is not null)
                        {
                            Fields[tableName].TryAdd(columnName, propertyEntry.GetDisplayName(columnName));
                            if (CurrentValues![tableName] is null)
                            {
                                CurrentValues![tableName] = new Dictionary<string, object?>();
                            }

                            if (OriginalValues![tableName] is null)
                            {
                                OriginalValues![tableName] = new Dictionary<string, object?>();
                            }

                            OriginalValues![tableName]!.Add(columnName, propertyEntry.OriginalValue);
                            CurrentValues![tableName]!.Add(columnName, propertyEntry.CurrentValue);
                        }
                    }

                    break;
            }
        }
    }

    private void AnalyzeAssociate(EntityEntry entityEntry, ILoggerOptions loggerOptions)
    {
        var columnNames = new List<string>();
        var principals = new List<IEntityType>();

        foreach (var prop in entityEntry.Properties)
        {
            if (prop.TryGetPrincipalTable(out var principalEntityType))
            {
                var tableName = principalEntityType?.ClrType.Name!;
                var displayName = principalEntityType?.GetDisplayName()!;

                Tables.Add(tableName, displayName);

                InitializeInternalEntry(entityEntry, tableName);

                principals.Add(principalEntityType!);
            }
            else
            {
                continue;
            }

            columnNames.Add(prop.GetColumnName()[..^2]);
        }

        var props = entityEntry.Properties.ToList();

        for (var i = 0; i < columnNames.Count; i++)
        {
            var buffer = Tables.ToArray();

            var inverseTableName = buffer[Tables.Count - i - 1].Key;
            var inverseDisplayeName = buffer[Tables.Count - i - 1].Value;
            var fieldInfo = principals[Tables.Count - i - 1].ClrType.GetProperty(columnNames[i])!;
            var fieldDisplayName = fieldInfo!.GetCustomAttribute<DisplayNameAttribute>(true)?.DisplayName;

            var propertyEntry = props[i];

            if (loggerOptions.PropertyFilters.Any(f => !f.Invoke(entityEntry, propertyEntry)))
            {
                continue;
            }

            if (propertyEntry.IsTemporary)
            {
                Fields[inverseTableName].TryAdd(columnNames[i], fieldDisplayName);

                if (TemporaryProperties![inverseTableName] is null)
                {
                    TemporaryProperties![inverseTableName] = new List<PropertyEntry>();
                }

                TemporaryProperties![inverseTableName]!.Add(propertyEntry);
                continue;
            }

            if (propertyEntry.Metadata.IsPrimaryKey())
            {
                Fields[inverseTableName].TryAdd(columnNames[i], fieldDisplayName);
                KeyValues[inverseTableName].Add(columnNames[i], propertyEntry.CurrentValue);
            }

            switch (entityEntry.State)
            {
                case EntityState.Added:
                    Fields[inverseTableName].TryAdd(columnNames[i], fieldDisplayName);
                    if (CurrentValues![inverseTableName] is null)
                    {
                        CurrentValues![inverseTableName] = new Dictionary<string, object?>();
                    }

                    CurrentValues![inverseTableName]!.Add(columnNames[i], propertyEntry.CurrentValue);
                    break;

                case EntityState.Deleted:
                    Fields[inverseTableName].TryAdd(columnNames[i], fieldDisplayName);
                    if (OriginalValues![inverseTableName] is null)
                    {
                        OriginalValues![inverseTableName] = new Dictionary<string, object?>();
                    }

                    OriginalValues![inverseTableName]!.Add(columnNames[i], propertyEntry.OriginalValue);
                    break;

                case EntityState.Modified:
                    if (propertyEntry.IsModified || loggerOptions.SaveUnChangedProperties)
                    {
                        if (propertyEntry.OriginalValue is not null && !propertyEntry.OriginalValue.Equals(propertyEntry.CurrentValue) ||
                            propertyEntry.OriginalValue is null && propertyEntry.CurrentValue is not null)
                        {
                            Fields[inverseTableName].TryAdd(columnNames[i], fieldDisplayName);
                            if (CurrentValues![inverseTableName] is null)
                            {
                                CurrentValues![inverseTableName] = new Dictionary<string, object?>();
                            }

                            if (OriginalValues![inverseTableName] is null)
                            {
                                OriginalValues![inverseTableName] = new Dictionary<string, object?>();
                            }

                            OriginalValues![inverseTableName]!.Add(columnNames[i], propertyEntry.OriginalValue);
                            CurrentValues![inverseTableName]!.Add(columnNames[i], propertyEntry.CurrentValue);
                        }
                    }

                    break;
            }
        }
    }
}

public class TrackerComparer : IEqualityComparer<TrackerEntry>
{
    public bool Equals(TrackerEntry? x, TrackerEntry? y)
    {
        return x is not null &&
               EqualityComparer<Dictionary<string, Dictionary<string, object?>>>.Default.Equals(x.KeyValues, y?.KeyValues) &&
               EqualityComparer<Dictionary<string, Dictionary<string, object?>>>.Default.Equals(x.Fields, y.Fields);
    }

    public int GetHashCode([DisallowNull] TrackerEntry obj)
    {
        return obj.GetHashCode();
    }
}